package server

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httputil"
	"os"
	"path/filepath"
	"strings"
)

var (
	debug bool
)

type OneDriver struct {
	DriverId string `json:"driver_id"`
}

func (s *Server) httpClient() (*http.Client, error) {
	tk, err := s.getToken()

	if err != nil {
		return nil, err
	}
	client := s.oauthConfig.Client(context.Background(), tk)
	return client, nil
}

func (s *Server) requestAndResponseObject(path string, method string, params interface{}, v interface{}) error {
	url := fmt.Sprintf("%s%s", endpoint, path)
	client, err := s.httpClient()
	if err != nil {
		return err
	}

	var (
		r io.Reader
	)

	if params != nil {
		switch params.(type) {
		case string:
			r = strings.NewReader(params.(string))
		case []byte:
			r = bytes.NewReader(params.([]byte))
		case io.Reader:
			r = params.(io.Reader)
		default:
			b, err := json.Marshal(params)
			if err != nil {
				return err
			}
			r = bytes.NewReader(b)
		}
	}

	req, err := http.NewRequest(method, url, r)
	if err != nil {
		return err
	}
	if method != http.MethodGet {
		req.Header.Add("Content-Type", "text/plain")
	}
	if debug {
		rb, _ := httputil.DumpRequest(req, true)
		log.Printf("request: %s", rb)
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if debug {
		rb, _ := httputil.DumpResponse(resp, true)
		log.Printf("response: %s", rb)
	}

	if v != nil {
		b, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return err
		}
		return json.Unmarshal(b, v)
	}

	return errors.New("receiver object can't be nil")
}

func (s *Server) Get(path string, params interface{}, v interface{}) error {
	return s.requestAndResponseObject(path, http.MethodGet, params, v)
}

func (s *Server) Post(path string, params interface{}, v interface{}) error {
	return s.requestAndResponseObject(path, http.MethodPost, params, v)
}

func (s *Server) Put(path string, params interface{}, v interface{}) error {
	return s.requestAndResponseObject(path, http.MethodPut, params, v)
}

func (s *Server) Delete(path string, params interface{}, v interface{}) error {
	return s.requestAndResponseObject(path, http.MethodDelete, params, v)
}

func (s *Server) InitOneDriver() error {
	s.OneDriver = OneDriver{}

	// list driver .
	var driverIdResp struct {
		Id string `json:"id"`
	}

	err := s.Get("/me/drive", nil, &driverIdResp)
	if err != nil {
		return errors.Wrap(err, "get /me/driver failed")
	}

	s.OneDriver.DriverId = driverIdResp.Id
	return nil
}

func (s *Server) findIfHasUploaded(f string) (bool, error) {
	url := fmt.Sprintf("/me/drive/root/search(q='%s')?select=id", f)
	var resp struct {
		Value map[interface{}]interface{} `json:"value"`
	}
	err := s.Get(url, nil, &resp)
	if err != nil {
		return false, err
	}
	if len(resp.Value) > 0 {
		return true, nil
	}
	return false, nil
}

func (s *Server) uploadLittleFile(filename string) error {

	name := filepath.Base(filename)
	url := fmt.Sprintf("/me/drive/root:/videos/%s:/content", name)

	f, err := os.OpenFile(filename, os.O_RDONLY, 0755)
	if err != nil {
		return err
	}
	err = s.Put(url, f, nil)
	return err
}

func (s *Server) uploadLargeFile(filename string) error {
	fname := filepath.Base(filename)
	// list driver .
	url := fmt.Sprintf("/drive/root:/videos/%s:/createUploadSession", fname)
	body := strings.NewReader(fmt.Sprintf(`{
  "item": {
    "@odata.type": "microsoft.graph.driveItemUploadableProperties",
    "@microsoft.graph.conflictBehavior": "rename",
    "name": "%s"
  }
}`, fname))
	var uploadUrl struct {
		UploadUrl string `json:"uploadUrl"`
	}
	err := s.Put(url, body, &uploadUrl)
	if err != nil {
		return errors.Wrap(err, "create upload session failed")
	}
	if uploadUrl.UploadUrl == "" {
		return errors.New("get upload url failed")
	}

	// upload this file .
	fsize := 10 * 1024 * 1024

	file, err := os.OpenFile(filename, os.O_RDONLY, 0755)
	if err != nil {
		return errors.Wrap(err, "reading file failed")
	}
	defer file.Close()

	log.Println("create upload session success. uploading.")
	buf := bufio.NewReaderSize(file, fsize)

	client, err := s.httpClient()
	var uploadedSize = 0

	for {
		b := make([]byte, fsize)
		n, err := buf.Read(b)
		if err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		req, err := http.NewRequest(http.MethodPut, uploadUrl.UploadUrl, bytes.NewReader(b[:n]))
		if err != nil {
			return err
		}
		fi, _ := file.Stat()
		req.Header.Add("Content-Length", fmt.Sprintf("%d", fi.Size()))
		req.Header.Add("Content-Range", fmt.Sprintf("bytes %d-%d/%d", uploadedSize, uploadedSize+n, fi.Size()))
		resp, err := client.Do(req)
		if err != nil {
			return err
		}
		resp.Body.Close()
		uploadedSize += n
	}
	return nil
}
